﻿using Devil;
using Excel;
using OfficeOpenXml;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.IO;
using TableCore.Core;
using TableCore.Plugin;

namespace TableCore
{
    public partial class GTFile
    {
        internal class TabHeader
        {
            public readonly GTType type;
            public readonly string name;
            public readonly string comment;
            public int colIndex;
            public TabHeader(GTType type, string name, string comment)
            {
                this.type = type;
                this.name = name;
                this.comment = comment;
                this.colIndex = -1;
            }
        }

        internal class Sheet
        {
            public readonly string name;
            public readonly List<TabHeader> cols;
            public int index;
            public readonly string srcFile;
            public readonly MinHeap<IExportData> datas;
            internal object[] dataRows;
            internal readonly MinHeap<int> invalidRows;

            public Sheet(string name, string srcFile)
            {
                this.name = name;
                cols = new List<TabHeader>();
                index = -1;
                datas = new MinHeap<IExportData>(256, (a, b) => a.DataId < b.DataId ? -1 : (a.DataId == b.DataId ? 0 : 1));
                this.srcFile = srcFile;
                invalidRows = new MinHeap<int>(64, (a, b) => a < b ? 1 : (a > b ? -1 : 0));
            }

            public TabHeader GetColume(string colname)
            {
                for (int i = 0; i < cols.Count; i++)
                {
                    var col = cols[i];
                    if (col.name == colname)
                        return col;
                }
                return null;
            }
        }

        internal class FindDataRowsCmd : ICommandParallelFor<int>
        {
            Sheet sheet;
            DataTable table;
            int count;
            internal FindClearRowsCmd cleaner;

            internal FindDataRowsCmd(DataTable table, Sheet sheet)
            {
                this.sheet = sheet;
                this.table = table;
                count = table.Rows.Count;
            }

            public void Execute(int index)
            {
                var data = sheet.datas[index];
                var id = Localization.ToString(data.DataId);
                int primaryIndex = -1;
                TabHeader primaryCol = null;
                bool asPrimary = data is IAsPrimary && !string.IsNullOrEmpty(((IAsPrimary)data).AsPrimaryCol);
                if (asPrimary)
                {
                    //cleanUp = false;
                    if (cleaner != null)
                        cleaner.clearUnusedData = false;
                    var pname = ((IAsPrimary)data).AsPrimaryCol;
                    primaryCol = sheet.GetColume(pname);
                    if (primaryCol != null && primaryCol.colIndex != -1)
                    {
                        var dataCols = data.ExportValues == null ? 0 : data.ExportValues.Length;
                        for (int k = 0; k < dataCols; k++)
                        {
                            var v = data.ExportValues[k];
                            if (v.name == pname)
                            {
                                primaryIndex = k;
                                break;
                            }
                        }
                    }
                }
                if (asPrimary && (primaryCol == null || primaryIndex == -1))
                {
                    sheet.dataRows[index] = null;
                    return;
                }

                DataRow updateRow = null;
                for (int r = START_ROW + 3; r < count; r++)
                {
                    string str;
                    var row = table.Rows[r];
                    if (asPrimary)
                    {
                        str = Utils.GetString(row[primaryCol.colIndex]);
                        if (str == data.ExportValues[primaryIndex].value)
                        {
                            if (sheet.dataRows[index] == null)
                            {
                                sheet.dataRows[index] = row;
                            }
                            else
                            {
                                List<DataRow> lst;
                                if (sheet.dataRows[index] is DataRow)
                                    lst = new List<DataRow>() { (DataRow)sheet.dataRows[index] };
                                else
                                    lst = (List<DataRow>)sheet.dataRows[index];
                                lst.Add(row);
                                sheet.dataRows[index] = lst;
                            }
                            //UpdateValues(sheet, row, data, true);
                        }
                        continue;
                    }
                    str = Utils.GetString(row[START_COL]);
                    if (str == id)
                    {
                        updateRow = row;
                        break;
                    }
                    //else if (updateRow == null && string.IsNullOrEmpty(str))
                    //{
                    //    updateRow = row;
                    //}
                }
                if (!asPrimary)
                {
                    if (updateRow == null)
                        sheet.dataRows[index] = "new";
                    else
                        sheet.dataRows[index] = updateRow;
                }
            }
        }

        internal class FindClearRowsCmd : ICommandParallelFor<int>
        {
            Sheet sheet;
            DataTable table;
            internal bool clearUnusedData;

            internal FindClearRowsCmd(DataTable table, Sheet sheet)
            {
                this.table = table;
                this.sheet = sheet;
            }

            public void Execute(int rowIndex)
            {
                rowIndex = rowIndex + START_ROW + 3;
                var row = table.Rows[rowIndex];
                int rid;
                bool isValid = true;
                var str = Utils.GetString(row[START_COL]);
                if (string.IsNullOrEmpty(str) || !int.TryParse(str, out rid))
                {
                    isValid = false;
                }
                else if (clearUnusedData)
                {
                    isValid = false;
                    for (int i = 0; !isValid && i < sheet.datas.Count; i++)
                    {
                        var dt = sheet.datas[i];
                        if (dt.DataId == rid)
                        {
                            isValid = true;
                        }
                    }
                }
                if (!isValid)
                {
                    lock (sheet.invalidRows)
                    {
                        sheet.invalidRows.Add(rowIndex);
                    }
                }
            }
        }

        internal class OutputFile : Disposable
        {
            readonly object _file_lock = new object();
            string mFilePath;
            //GTFile mWorkFile;
            //List<IExportData> mExports;
            DataSet mDatas;
            Dictionary<int, Sheet> mSheets;
            bool mCleanup;
            bool mExport;
            public bool ShouldExport { get { return mExport; } }
            internal ParallelDispatcher.Handler mOpenCmd;
            public string ExcelFile { get { return mFilePath; } }
            public OutputFile(string file, bool cleanup)
            {
                //mWorkFile = workFile;
                mFilePath = file;
                mSheets = new Dictionary<int, Sheet>();
                mCleanup = cleanup;
                //mExports = new List<IExportData>();
            }

            public void MarkShouldExport()
            {
                mExport = true;
            }

            Sheet GetSheet(IExportData data, string createByFile)
            {
                var id = StringUtil.IgnoreCaseToHash(data.ExportFile.excelSheet);
                Sheet sheet;
                if (!mSheets.TryGetValue(id, out sheet))
                {
                    if(!string.IsNullOrEmpty(createByFile))
                    {
                        sheet = new Sheet(data.ExportFile.excelSheet, createByFile);
                        mSheets.Add(id, sheet);
                        return sheet;
                    }
                    return null;
                }
                return sheet;
            }

            public bool Add(string fromFile, IExportData data)
            {
                var values = data.ExportValues;
                if (values == null || values.Length == 0)
                    return false;
                lock (_lock)
                {
                    //mExports.Add(data);
                    Sheet sheet = GetSheet(data, fromFile);
                    if (sheet.datas.ComparisonContains(data, false))
                        return false;
                    sheet.datas.Add(data);
                    for (int i = 0; i < values.Length; i++)
                    {
                        var v = values[i];
                        if (v.type == null || string.IsNullOrEmpty(v.name))
                            continue;
                        var col = sheet.GetColume(v.name);
                        if (col == null)
                        {
                            col = new TabHeader(v.type, v.name, v.comment);
                            if (col.name == "id")
                                sheet.cols.Insert(0, col);
                            else
                                sheet.cols.Add(col);
                        }
                    }
                    //if (add)
                    //    Debug.LogFormat("Export file: {0}", data.ExportFile);
                }
                return true;
            }

            // 加载已经存在的表数据
            internal void OpenFile()
            {
                if (string.IsNullOrEmpty(mFilePath) || !File.Exists(mFilePath))
                    return;
                    //throw new FileNotFoundException("GTFile Can't open file: " + mFilePath);
                using (var stream = new FileStream(mFilePath, FileMode.Open))
                {
                    IExcelDataReader reader = null;
                    try
                    {
                        if (mFilePath.EndsWith(".xls"))
                        {
                            reader = ExcelReaderFactory.CreateBinaryReader(stream);
                        }
                        else if (mFilePath.EndsWith(".xlsx"))
                        {
                            reader = ExcelReaderFactory.CreateOpenXmlReader(stream);
                        }
                        reader.IsFirstRowAsColumnNames = false;
                        while (reader.Read()) { }
                        var data = reader.AsDataSet();
                        stream.Close();
                        mDatas = data;
                    }
                    catch (Exception e)
                    {
                        Debug.LogFormat("open \"{0}\" error.\n{1}", mFilePath, e);
                    }


                }
            }

            // 初始化表头数据
            internal void InitTables()
            {
                lock (_file_lock)
                {
                    if (mDatas == null)
                    {
                        mDatas = new DataSet();
                    }
                    foreach (var sheet in mSheets.Values)
                    {
                        DataTable tab = null;
                        var tables = mDatas.Tables;
                        for (int i = 0; i < tables.Count; i++)
                        {
                            if (tables[i].TableName == sheet.name)
                            {
                                tab = tables[i];
                                sheet.index = i;
                                break;
                            }
                        }
                        if (tab == null)
                        {
                            sheet.index = mDatas.Tables.Count;
                            tab = mDatas.Tables.Add(sheet.name);
                        }
                        var rows = tab.Rows;
                        while (rows.Count < 3)
                        {
                            var row = tab.NewRow();
                            rows.Add(row);
                        }
                        var cols = tab.Columns;
                        if (cols.Count < 2)
                            cols.Add();

                        var row0 = tab.Rows[START_ROW];
                        var row1 = tab.Rows[START_ROW + 1];
                        var row2 = tab.Rows[START_ROW + 2];
                        row0[COMMENT_COL] = "comment";
                        if (!string.IsNullOrEmpty(sheet.srcFile))
                            row1[COMMENT_COL] = sheet.srcFile;

                        for (int i = 0; i < sheet.cols.Count; i++)
                        {
                            var col = sheet.cols[i];
                            if (string.IsNullOrEmpty(col.name) || col.type == null)
                                continue;
                            int startI = -1;
                            for (int c = START_COL; col.colIndex == -1 && c < cols.Count; c++)
                            {
                                var str = Utils.GetString(row0[c]);
                                if (string.IsNullOrEmpty(str))
                                {
                                    startI = c;
                                }
                                else if (str == col.name)
                                {
                                    startI = c;
                                    break;
                                }
                            }
                            if (startI == -1)
                            {
                                cols.Add();
                                startI = cols.Count - 1;
                            }
                            row0[startI] = col.name;
                            row1[startI] = col.type.Name;
                            row2[startI] = string.IsNullOrEmpty(col.comment) ? col.name : col.comment;
                            col.colIndex = startI;
                        }
                    }

                }
            }

            void UpdateValues(Sheet sheet, DataRow row, IExportData data, bool asPrimary)
            {
                var len = data.ExportValues == null ? 0 : data.ExportValues.Length;
                for (int c = 0; c < len; c++)
                {
                    var value = data.ExportValues[c];
                    var col = sheet.GetColume(value.name);
                    if (col == null || col.colIndex == -1)
                        continue;
                    if (asPrimary && StringUtil.EqualIgnoreCase(col.name, "id"))
                        continue;
                    row[col.colIndex] = value.value;
                }
            }

            internal IEnumerator UpdateContents()
            {
                if (mDatas == null)
                    mDatas = new DataSet();
                InitTables();
                var jobs = new ParallelDispatcher.CombinedHandler("update export file");
                foreach (var sheet in mSheets.Values)
                {
                    if (sheet.index == -1)
                        continue;
                    sheet.invalidRows.Clear();
                    sheet.dataRows = new object[sheet.datas.Count];
                    var table = mDatas.Tables[sheet.index];
                    var cmd = new FindDataRowsCmd(table, sheet);
                    var cleaner = new FindClearRowsCmd(table, sheet);
                    cleaner.clearUnusedData = mCleanup;
                    cmd.cleaner = cleaner;
                    var job = cmd.Schedule(sheet.datas.Count);
                    if(table.Rows.Count > START_ROW + 3)
                        job = cleaner.Schedule(table.Rows.Count - START_ROW - 3, job);
                    jobs.Combine(job);
                }
                yield return jobs;
                foreach (var sheet in mSheets.Values)
                {
                    if (sheet.index == -1)
                        continue;
                    var table = mDatas.Tables[sheet.index];
                    for (int i = 0; i < sheet.datas.Count; i++)
                    {
                        var row = sheet.dataRows[i];
                        if (row == null)
                            continue;
                        var dt = sheet.datas[i];
                        bool asPrimary = dt is IAsPrimary && !string.IsNullOrEmpty(((IAsPrimary)dt).AsPrimaryCol);
                        if (row is DataRow)
                        {
                            if (!asPrimary)
                                ((DataRow)row)[COMMENT_COL] = dt.Comment ?? "";
                            UpdateValues(sheet, (DataRow)row, dt, asPrimary);
                        }
                        else if (row is List<DataRow>)
                        {
                            foreach (var r in (List<DataRow>)row)
                            {
                                UpdateValues(sheet, r, dt, asPrimary);
                            }
                        }
                        else if (row != null)
                        {
                            var r = table.NewRow();
                            table.Rows.Add(r);
                            if (!asPrimary)
                                r[COMMENT_COL] = dt.Comment ?? "";
                            UpdateValues(sheet, r, dt, asPrimary);
                        }
                    }
                    while (sheet.invalidRows.Count > 0)
                    {
                        var r = sheet.invalidRows.RemoveTop();
                        table.Rows.RemoveAt(r);
                    }
                }
                SaveDatas();
                Debug.Log($"Update Contents: {mFilePath}");
            }

            // 更新数据
            internal void UpdateDatas()
            {
                foreach (var sheet in mSheets.Values)
                {
                    if (sheet.index == -1)
                        continue;
                    var table = mDatas.Tables[sheet.index];

                    var count = table.Rows.Count;
                    bool cleanUp = mCleanup;
                    for (int i = 0; i < sheet.datas.Count; i++)
                    {
                        var data = sheet.datas[i];
                        var id = Localization.ToString(data.DataId);
                        int primaryIndex = -1;
                        TabHeader primaryCol = null;
                        bool asPrimary = data is IAsPrimary && !string.IsNullOrEmpty(((IAsPrimary)data).AsPrimaryCol);
                        if (asPrimary)
                        {
                            cleanUp = false;
                            var pname = ((IAsPrimary)data).AsPrimaryCol;
                            primaryCol = sheet.GetColume(pname);
                            if (primaryCol != null && primaryCol.colIndex != -1)
                            {
                                var dataCols = data.ExportValues == null ? 0 : data.ExportValues.Length;
                                for (int k = 0; k < dataCols; k++)
                                {
                                    var v = data.ExportValues[k];
                                    if (v.name == pname)
                                    {
                                        primaryIndex = k;
                                        break;
                                    }
                                }
                            }
                        }
                        if (asPrimary && (primaryCol == null || primaryIndex == -1))
                        {
                            continue;
                        }

                        DataRow updateRow = null;
                        for (int r = START_ROW + 3; r < count; r++)
                        {
                            string str;
                            var row = table.Rows[r];
                            if(asPrimary)
                            {
                                str = Utils.GetString(row[primaryCol.colIndex]);
                                if(str == data.ExportValues[primaryIndex].value)
                                {
                                    UpdateValues(sheet, row, data, true);
                                }
                                continue;
                            }
                            str = Utils.GetString(row[START_COL]);
                            if (str == id)
                            {
                                updateRow = row;
                                break;
                            }
                            else if (updateRow == null && string.IsNullOrEmpty(str))
                            {
                                updateRow = row;
                            }
                        }
                        if (!asPrimary)
                        {
                            if (updateRow == null)
                            {
                                updateRow = table.NewRow();
                                table.Rows.Add(updateRow);
                            }
                            updateRow[COMMENT_COL] = data.Comment ?? "";
                            UpdateValues(sheet, updateRow, data, false);
                        }
                    }
                    // 清除未被引用到的数据
                    if (cleanUp)
                    {
                        for (int r = count - 1; r >= START_ROW + 3; r--)
                        {
                            var row = table.Rows[r];
                            int rid;
                            bool isValid;
                            if (!int.TryParse(Utils.GetString(row[START_COL]), out rid))
                            {
                                isValid = false;
                            }
                            else
                            {
                                isValid = false;
                                for (int i = 0; !isValid && i < sheet.datas.Count; i++)
                                {
                                    var dt = sheet.datas[i];
                                    if (dt.DataId == rid)
                                        isValid = true;
                                }
                            }
                            if (!isValid)
                                table.Rows.RemoveAt(r);
                        }
                    }
                }

            }

            internal void SaveDatas()
            {
                var folder = Utils.GetFolderName(mFilePath);
                if (!Directory.Exists(folder))
                    Directory.CreateDirectory(folder);
                var excel = new ExcelPackage(new FileInfo(mFilePath));
                foreach (var sheet in mSheets.Values)
                {
                    ExcelWorksheet excelSheet = null;
                    foreach (var book in excel.Workbook.Worksheets)
                    {
                        if (book.Name == sheet.name)
                        {
                            excelSheet = book;
                            break;
                        }
                    }
                    var tab = mDatas.Tables[sheet.index];
                    var rows = tab.Rows;
                    var cols = tab.Columns;
                    double[] colsize = new double[ParallelUtils.Max(4, cols.Count)];
                    if (excelSheet == null)
                    {
                        excelSheet = excel.Workbook.Worksheets.Add(sheet.name);
                        colsize[0] = 25;
                        colsize[1] = 20;
                        colsize[2] = 20;
                        for (int i = 3; i < colsize.Length; i++)
                        {
                            colsize[i] = 30;
                        }
                    }
                    else
                    {
                        for (int i = 0; i < colsize.Length; i++)
                        {
                            colsize[i] = ParallelUtils.Max(20, (int)excelSheet.Column(i + 1).Width);
                            if (colsize[i] > 80)
                                colsize[i] = 80;
                        }
                    }
                    excelSheet.Cells.Clear();
                    var rownum = rows.Count;
                    var colnum = cols.Count;
                    for (int r = 0; r < rownum; r++)
                    {
                        for (int c = 0; c < colnum; c++)
                        {
                            excelSheet.Cells[r + 1, c + 1].Value = rows[r][c];
                        }
                    }
                    for (int i = 0; i < colsize.Length; i++)
                    {
                        var col = excelSheet.Column(i + 1);
                        col.Width = colsize[i];
                    }
                }
                excel.Save();
                excel.Dispose();
                mDatas.Dispose();
                mDatas = null;
                Debug.LogFormat("{0} file: {1}", ShouldExport ? "export" : "merge", mFilePath);
            }

            protected override void OnDisposing()
            {
                if(mDatas != null)
                {
                    mDatas.Dispose();
                    mDatas = null;
                }
                mSheets.Clear();
            }
        }
    }
}
